/**
 * Project: d20Engine
 * Created: Oct 21, 2006 by bebopJMM
 */
package org.rollinitiative.d20;

import java.util.ArrayList;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * AdjustableValue manages a single score, such as an ability or hitPoints. It tracks all
 * adjustments, however temporary or permanent, against the base score. It notifies all subscribers
 * to changes in the value being managed.
 * 
 * @author bebopjmm
 * 
 */
public class AdjustableValue implements AdjustmentListener
{
    static final Log LOG_ = LogFactory.getLog(AdjustableValue.class);

    public static final int DEFAULT_VALUE = 0;

    /**
     * The baseValue is the raw score without any modifiers.
     */
    private int baseValue_;

    /**
     * The currentValue is the ability score with all currently applicable modifiers
     */
    private int currentValue_;

    /**
     * The current set of adjustments effecting this adjustable value.
     */
    List<Adjustment> adjustments_;

    /**
     * Subscribers to changes in currentValue
     */
    List<AdjustableValueListener> subscribers_;


    /**
     * Instantiates a new AdjustableValue, setting both base and current values to the provided
     * baseValue.
     * 
     * @param baseValue
     * 
     * @since incr-0.1
     */
    public AdjustableValue(int baseValue)
    {
        this.baseValue_ = baseValue;
        this.currentValue_ = baseValue;
        adjustments_ = new ArrayList<Adjustment>();
        subscribers_ = new ArrayList<AdjustableValueListener>();
    }


    /**
     * @return the baseValue
     * @since incr-0.1
     */
    public int getBase()
    {
        return baseValue_;
    }


    /**
     * @param baseValue the baseValue to set
     * @since incr-0.1
     */
    public synchronized void setBase(int baseValue)
    {
        this.baseValue_ = baseValue;
        recalcValue();
        notifyOfChange();
    }


    /**
     * @return the current ability value
     * @since incr-0.1
     */
    public synchronized int getCurrent()
    {
        return currentValue_;
    }


    /**
     * Adds the adjustment to the value calculation set, updating the currentValue and notifying all
     * subscribed listeners. No action is taken if the provided Adjustment is already part of the
     * list.
     * 
     * @param adjustment new Adjustment to include in value calculation
     */
    public synchronized void addAdjustment(Adjustment adjustment)
    {
        if (adjustments_.contains(adjustment)) {
            LOG_.warn("Attempt to add duplicate ability adjustment: " + adjustment.description_);
            return;
        }
        LOG_.debug("Adding Adjustment: " + adjustment.description_ + "of value: " + adjustment.getValue());
        adjustments_.add(adjustment);
        adjustment.subscribe(this);
        currentValue_ += adjustment.getValue();
        LOG_.debug("Current value =" + currentValue_);
        notifyOfChange();
    }


    /**
     * Removes the adjustment from the value calculation set, updating the currentValue and
     * notifying all subscribed listeners. No action is taken if the provide Adjustment is not part
     * of the list.
     * 
     * @param adjustment Adjustment to be removed from value calculation.
     */
    public synchronized void removeAdjustment(Adjustment adjustment)
    {
        if (!adjustments_.contains(adjustment)) {
            LOG_.warn("Attempt to remove adjustment not associated to ability: "
                    + adjustment.description_);
            return;
        }
        LOG_.debug("Removing Adjustment: " + adjustment.description_ + " of value: " + adjustment.getValue());
        adjustments_.remove(adjustment);
        adjustment.unsubscribe(this);
        currentValue_ -= adjustment.getValue();
        LOG_.debug("Current value =" + currentValue_);
        notifyOfChange();
    }


    /**
     * @return the array of all Adjustments currently used in the value calculation.
     */
    public synchronized Adjustment[] getAdjustments()
    {
        Adjustment[] adjArray = new Adjustment[adjustments_.size()];
        return adjustments_.toArray(adjArray);
    }


    public void subscribe(AdjustableValueListener listener)
    {
        synchronized (subscribers_) {
            if (!subscribers_.contains(listener)) {
                subscribers_.add(listener);
            }
        }
    }


    public void unsubscribe(AdjustableValueListener listener)
    {
        synchronized (subscribers_) {
            if (subscribers_.contains(listener)) {
                subscribers_.remove(listener);
            }
        }
    }


    /*
     * (non-Javadoc)
     * 
     * @see
     * org.rollinitiative.d20.AdjustmentListener#valueChanged(org.rollinitiative.d20.Adjustment)
     */
    @Override
    public void valueChanged(Adjustment adjustment)
    {
        int oldVal = currentValue_;
        recalcValue();
        if (oldVal != currentValue_) {
            notifyOfChange();
        }
        else {
            LOG_.debug("No change in currentValue, not notifying subscribers");
        }
    }


    protected void recalcValue()
    {
        LOG_.debug("recalculating current value: baseValue + sum of adjustments");
        currentValue_ = baseValue_;
        for (Adjustment adjustment : adjustments_) {
            currentValue_ += adjustment.getValue();
        }
    }


    protected void notifyOfChange()
    {
        synchronized (subscribers_) {
            LOG_.debug("Notifying subcribers of currentValue change, totalSubscribers = "
                    + subscribers_.size());
            for (AdjustableValueListener subscriber : subscribers_) {
                subscriber.valueChanged(this);
            }
        }
    }
}
